--[[
	We try very loosely to match the security model of wow.

	We just can't completly emulate it in pure lua, so we're going for
	this approximation.

	we use two information for security. The first is the level, wich
	should be "SECURE" (for FrameXML and Blizzard addons) and "INSECURE"
	for the rest, and the taint, which is the name of the addon.

	Any global variable set during the simulation is "marked" with the current
	level and taint. This information is kept in the security.objects table.

	Every time an "INSECURE" global variable is loaded, the current security model is
	switched to "INSECURE".

the following simulated functions are defined here :

	issecure()
	issecurevariable()
	securecall()
	hooksecurefunc()
]]

Security = {
	combat = nil,
	taint = nil,
	level = "INSECURE",
	hooks = {},
}

local security = Security

security.objects = setmetatable({}, { __mode="k" })

function security:initialize()
	local t = { taint = nil, level = "SECURE" }
	for _, value in pairs(api.global) do
		self.objects[value] = t
	end
end

function security:CombatLockdown(value)
	self.combat = value
	UI.LockFrames(value)
end

function security:set(taint, level)
	assert(type(taint) == "string")
	assert(level == "SECURE" or level == "INSECURE")

	self.taint = taint
	self.level = level
end

function security:setaccess(value)
	local ti = self.objects[value]
	if ti and ti.level == "INSECURE" then
		self:set(ti.taint, ti.level)
	end
end

function security:setobject(value)
	local v = self.objects[value]
	if not v then
		v = {}
		self.objects[value] = v
	end
	v.taint = self.taint
	v.level = self.level
	if v.level ~= "SECURE" then
		local i = debug.getinfo(3, "Sl")
		v.source = i.short_src
		v.line = i.currentline
	end
end

function security:setfromobject(value, taint, level)
	local v = self.objects[value]
	if v then
		taint = v.taint
		level = v.level
	end
	if level == "INSECURE" then
		self:set(taint, level)
	end
end

function security:DumpTaints()
	local f = io.open("taints.txt", "w")
	for name, value in pairs(api.current) do
		local i = security.objects[value]
		if i and i.level ~= "SECURE" then
			f:write(string.format("%s %s %s %s:%s\n", name, i.taint, i.level, i.source or "unknown", i.line or 0))
		end
	end
	f:close()
end


function api.global.issecure()
	return security.level == "SECURE"
end

function api.global.forceinsecure()
        security.level = "INSECURE"
end

function api.global.wipe(tbl)
	local k = next(tbl)
	while k do
		tbl[k] = nil
		k = next(tbl)
	end
end
api.global.table.wipe = api.global.wipe

function api.global.scrub(...)
	local temp = {}
	local n = select("#", ...)
	for i = 1, n do
		local v = select(i, ...)
		local t = type(v)
		if t == "table" or t == "userdata" or t == "thread" then
			v = nil
		end
		temp[i] = v
	end
	return unpack(temp, 1, n)
end

function api.global.issecurevariable(value, index)
	if type(value) == "string" then
		value = api.global.getglobal(value)
	end
	if index and type(value) == "table" then
		value = value[index]
	end

	local ti = security.objects[value]
	local taint, level
	if ti then
		taint, level = ti.taint, (ti.level == "SECURE")
	else
		taint = "<UNKNOWN>"
	end
	if level then
		return 1
	else
		return nil, taint
	end
end

function api.global.securecall(f, ...)
	if type(f) == "string" then
		local r = api.global.getglobal(f)
		if not r then
			log("EE unable to securely call inexistant function %s", f)
		end
		f = r
	end
	assert(type(f) == "function", "Invalid parameter to securecall")
	local taint, level = security.taint, security.level
	f(...)
	security.taint, security.level = taint, level
end

function api.global.hooksecurefunc(name, hook, ...)
	if type(name) == "string" then
		local f = api.global.getglobal(name)
		assert(type(f) == "function", "invalid parameter to hooksecurefunc")
		security:setobject(hook)
		local handler = security.hooks[f]
		if not handler then
			handler = {
				hooks = {},
			}
			security.hooks[f] = handler
			local call = api.call
			local after = function (params, ...)
				local taint, level = security.taint, security.level
				for _, hook in ipairs(handler.hooks) do
					call(function () hook(unpack(params)) end)
				end
				security.taint, security.level = taint, level
				return ...
			end
			local func = function (...)
				return after({...}, f(...))
			end
			setfenv(func, api.global._G)
			security.objects[func] = { taint = "<HOOK>", level = security.objects[f].level }
			handler.func = func
			api.current[name] = func
		end
		table.insert(handler.hooks, hook)
	else --if type(name) == "table" then
		assert(type(name) == "table", "Invalid parameter type to hooksecurefunc")
		local t = name
		name = hook
		local f = rawget(t, name)
		hook = ...
		if not hook then
			log("WW inexistent hook in hooksecurefunc")
			return
		end
		if not f then
			local ori = t[name]
			assert(ori, "invalid method name to hooksecurefunc")
			f = function (...) return ori(...) end
			rawset(t, name, f)
			security.objects[f] = security.objects[ori] or { taint = "<UNKNOWN>", level = "INSECURE" }
		end
		security:setobject(hook)
		local handler = security.hooks[f]
		if not handler then
			handler = {
				hooks = {},
			}
			security.hooks[f] = handler
			local call = api.call
			local func = function (...)
				local results = {f(...)}
				local params = {...}
				local taint, level = security.taint, security.level
				for _, hook in ipairs(handler.hooks) do
					call(function () hook(unpack(params)) end)
				end
				security.taint, security.level = taint, level
				return unpack(results)
			end
			setfenv(func, api.global._G)
			security.objects[func] = { taint = "<HOOK>", level = security.objects[f].level }
			handler.func = func
			t[name] = func
		end
		table.insert(handler.hooks, hook)
	end
end
